Django API 回應,常常是對 Model 物件(即 db 資料)內容進行一定的篩選與加工。
比如「取得單一文章資訊」API,實際上就是從Post
物件挑選欄位,再進行序列化。
這個過程中,我們需要考慮如何將模型物件轉換為 API 的回應結構,同時保持程式碼的可維護性與靈活。
對此,Django REST framework(以下簡稱 DRF)提供了非常實用的「特製」序列化器——ModelSerializer
,可說是 DRF 開發者必學的核心功能。
Django Ninja 雖然也有類似的實踐——ModelSchema
,對我而言卻是雞肋般的存在,我幾乎不曾使用。
這樣的差異,無疑是兩者的核心設計理念不同所導致。
我們曾在第 3 篇中討論過,兩者在功能上的主要區別。本文將透過「Django 模型物件的序列化」這個頗具代表性的議題,說明「為何相比於 DRF,我更喜歡寫 Django Ninja」。
DRF 中的ModelSerializer
是個非常強大的工具,它能夠自動將 Django 模型轉換為 API 需要的資料結構——序列化器,大大簡化了「為序列化器定義欄位」的過程。
附帶一提,DRF 序列化器,相當於 Django Ninja 所使用的 Schema,兩者的概念大同小異,都是用於資料的驗證與序列化。
如果我們把「取得單一文章資訊」API 回應用ModelSerializer
改寫,它將長這樣:
from rest_framework import serializers
# Author 序列化器
class AuthorSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'email']
# Post 序列化器
class PostSerializer(serializers.ModelSerializer):
author = AuthorSerializer() # 嵌套的 Author 序列化器
class Meta:
model = Post
fields = ['id', 'title', 'content', 'author', 'created_at', 'updated_at']
如你所見,透過ModelSerializer
,我們只需要少少的程式碼便能定義完序列化器,從而避免了手動設定的重複與麻煩。
然而,這樣的方便也帶來一定的隱憂。
因為不用自己定義欄位,所以ModelSerializer
幫你做了許多欄位的隱式轉換——從 Django Model 欄位轉換為序列化器欄位。
為何說「隱式」呢?因為自動轉換後的序列化器欄位,其欄位的型別、特性、是否唯讀(read_only
)等細節,你未必清楚。
換言之,ModelSerializer
不僅會自動生成欄位,還會自動推斷欄位的型別、屬性、屬性的參數等。
這樣講有點抽象,對於沒寫過 DRF 的讀者可能不好太理解。我們直接看一個例子:
from django.db import models
class Person(models.Model):
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)
這是一個超簡單的 Django Model,我引用自 Django 官方文件。
它有兩個欄位first_name
和last_name
,實際上它還有一個 Django 自動生成的id
欄位,在程式碼中沒有顯示。
使用ModelSerializer
,我們可以這樣定義序列化器:
from rest_framework import serializers
from .models import Person
class PersonSerializer(serializers.ModelSerializer):
class Meta:
model = Person
fields = ['id', 'first_name', 'last_name']
程式碼很簡單,但它背後的「魔法」卻很多。
具體而言,實際上的序列化器和欄位長這樣:
from rest_framework import serializers
class PersonSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
first_name = serializers.CharField(max_length=30)
last_name = serializers.CharField(max_length=30)
def create(self, validated_data):
return Person.objects.create(**validated_data)
def update(self, instance, validated_data):
instance.first_name = validated_data.get('first_name', instance.first_name)
instance.last_name = validated_data.get('last_name', instance.last_name)
instance.save()
return instance
有沒有覺得有點吃驚?
其中,id
欄位被自動加上了read_only=True
,first_name
和last_name
則被自動加上了max_length=30
。
這還是在 Django Model 的設計與欄位參數相對簡單的情況下,當 Model 欄位更複雜時,ModelSerializer
的「魔法」也會變得更加複雜。
它背後有很多轉換邏輯,讓開發者在某些情況下必須去理解這些「隱藏規則」——因為這個推斷有時可能不符合你的需求,導致你需要手動覆寫。
總之,自動推斷與轉換固然省去了手動設定的麻煩,但當你需要調整某些細節,或理解具體的轉換邏輯時,這種隱式行為可能會讓你感到困惑。
在實際開發中,這種隱式轉換的「魔法」會讓開發者失去對轉換過程的理解與掌控。你很可能會發現,序列化的結果和你想的並不完全一致!
此時我們往往需要翻閱 DRF 的官方文件來理解內部如何處理這些欄位轉換,但也不是每個細節都寫得清楚明白。
對開發者而言,特別是在處理複雜 API 時,會明顯增加學習和維護成本。
以上正是我的經驗!
即使寫了 2 年 DRF,遇到序列化問題,我還是很常需要重新查看文件。
Django Ninja 的 ModelSchema 相較於 ModelSerializer,則顯得「陽春」許多。
怎麼說?我們看一下官方文件中的例示:
from django.contrib.auth.models import User
from ninja import ModelSchema
class UserSchema(ModelSchema):
class Meta:
model = User
fields = ['id', 'username', 'first_name', 'last_name']
# Will create schema like this:
#
# class UserSchema(Schema):
# id: int
# username: str
# first_name: str
# last_name: str
說它陽春,因為它只會幫你自動轉換、定義欄位的「型別」而已。其他欄位細節,比如max_length
,都要靠Field
來設定——ModelSchema 不會幫你做這些。
而 DRF 的ModelSerializer
,如前所述,則是會「做更多」。
既然 ModelSchema 的自動轉換相對單純,那為何我還是不建議使用呢?有兩個理由。
其中第一個理由,就是標題所說「為何我更偏愛 Django Ninja」的理由。
Django Ninja 更強調開發者對 API 結構的掌握,而 DRF 則偏向於提供高度整合且便利的工具。
這種差異反映在它們對待 Django 模型序列化的方式上,也影響了開發者在使用這兩個框架時的風格和思維方式。
我們可以發現, DRF 幾乎是一個「為 Django 高度定製」的 API 開發工具。
這種緊密的結合雖然帶來了便利性,但也意味著 DRF 在很大程度上依賴於 Django 的內部結構和功能。不管是 Generic views,還是本文的 ModelSerializer,都是如此。
高耦合的優點就是你可以少做很多事,而代價則是你要很了解自己在做什麼。
相較於 DRF,Django Ninja 與 Django 的耦合程度則要低得多。
在我看來,Django Ninja 更偏好「明確優於隱晦」,Django Ninja 的 Schema 定義是基於 Pydantic,它要求開發者明確定義每個欄位,無論是輸入還是輸出。
雖然這樣相對繁瑣,但它帶來的好處是顯而易見的。
首先,手動定義 Schema 讓開發者對資料結構有著絕對的掌控權。沒有任何隱藏規則或暗箱操作,一切都清晰可見。
其次,這種方法有效地降低了模型層與 API 層之間的耦合。在實際開發中,模型設計可能會隨著需求變化而更新,但這不應該直接影響到 API。
總的來說,Django Ninja 強調以 Schema 為核心的控制,讓 API 的設計更具穩定性和靈活性,並賦予開發者對資料流的完全掌控。
在第 18 篇,我們會詳細討論 Schema 欄位設定對 API 文件的影響。
簡言之,如果使用 ModelSchema,那麼渲染出來的 API 文件將會相當陽春。
這並不符合我對 API 文件清晰與明確性的追求。
不可否認,Django REST framework 有一些非常方便且貼心的設計,比如上一篇提到的source=
參數,它直觀而優雅。
Django Ninja 則要求開發者,盡可能手動定義每個欄位,減少模型與 API 層的耦合,這更符合 Python 哲學中的「明確優於隱晦」,同時避免隱式行為帶來的潛在問題。
這正是我更偏愛 Django Ninja 的原因。
Django Ninja 對明確性的追求,讓我在開發和維護 API 時,多數時候感覺更加輕鬆。
本文同步發表於我的部落格——Code and Me
DRF:
class AuthorSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ['id', 'username', 'email']
Ninja:
class UserSchema(ModelSchema):
class Meta:
model = User
fields = ['id', 'username', 'first_name', 'last_name']
Django Ninja 的 ModelSchema 相較於 ModelSerializer,則顯得「單純」許多。
都沒用過的我,覺得兩個都長差不多?! 只知道繼承的東西不同
你的問題一語驚醒夢中人!
我沒有發現文章中存在的這個問題(指兩者很像,看不出 ModelSchema 哪裡「相對單純」了),顯然我已經對 DRF 習以為常,造成創作上的盲點
經過一番修改,我在「ModelSerializer 的隱憂」中加上了「自動轉換的範圍」一整段,並在「ModelSchema」段落中多解釋了一點其中的「單純」所在
你再看看有沒有解惑到🥹
class AccountSerializer(serializers.ModelSerializer):
class Meta:
model = Account
fields = ['id', 'account_name', 'users', 'created']
Model 中的
name
欄位,被ModelSerializer轉換為序列化器的name欄位時,自動被加上了allow_blank=True、max_length=100、required=False等屬性:
name 欄位是指 account_name ?
from django.contrib.auth.models import User
from ninja import ModelSchema
class UserSchema(ModelSchema):
class Meta:
model = User
fields = ['id', 'username', 'first_name', 'last_name']
# Will create schema like this:
#
# class UserSchema(Schema):
# id: int
# username: str
# first_name: str
# last_name: str
下面註解部分 Schema 是 pydantic 模式我是懂的,看起來像是說明上面那段幾乎等於下面這段
但我好奇的是它(Django)是怎知道每個 field 的 type? 為什知道 id
就是 int 而不是 str,為什知道 username
是個 str 而不是其他 type?
還是說這些 type 都是 Runtime 時候才被確定?
如果是 Runtime 時候才確定那就可以明白這樣的寫法確實無法自動產出 API 文件(至少每個欄位的型別是無法預先知道的)
然後這樣一比也確實明白 DRF 的缺點,確實很大程度上違背了 Python之禪的第二段:
Explicit is better than implicit. (明瞭優於隱晦。)
DRF 背後做了太多事情,除非你明確知道,否則太多東西不夠明顯。就算你知道,你的同事也不一定知道,或者未來的你可能會忘記。
Type Hint 寫久了真的會很難適應沒寫 Type Hint 的程式碼,IDE 各種無法支援自動補全,讀 code 也很難過。
假設有公司找我,如果我知道他們公司內部程式碼都不寫 Type Hint,我一定不去
name 欄位是指 account_name ?
name 就是 name XD,Account Model 有這個屬性,而且屬性上有一些欄位參數設定
只是官方文件似乎沒有為這個 Account Model 舉例(至少我找不到),直接就進序列化器
但我好奇的是它(Django)是怎知道每個 field 的 type? 為什知道 id 就是 int 而不是 str,為什知道 username 是個 str 而不是其他 type?
DRF 就是去參考 Django Model 中關於欄位的設定,再轉換成序列化器的格式參數,其中的隱性規則很多!
比如 Model 欄位中有null=True
,它就會轉換成required=False
、是 primary key 的話,就會加上required=True
和editable=False
,很多很多!
就是被這些隱性轉換規則搞得要反覆查閱文件,做了一堆筆記——但非常容易忘記😂
Type Hint 寫久了真的會很難適應沒寫 Type Hint 的程式碼,IDE 各種無法支援自動補全,讀 code 也很難過。
我認同,習慣了 type hints,如果不寫,真的很為難,誰用誰知道!
假設有公司找我,如果我知道他們公司內部程式碼都不寫 Type Hint,我一定不去
灰常支持!哈哈哈
Account Model, Django Model
突然好多新詞,Account Model (還是AccountNameModel?),是根據 account_name 這個欄位名稱 背後動態生成的 一個 class 嗎? 且有一個自己的name屬性?還是真的有一個專有名詞叫這個,跟欄位名稱無關
Django Model 欄位設定? 是還要去別的地方設定這些自定義 Field 的欄位名稱和型別嗎? 這樣聽起來好像還是在別邊要寫型別提示? 那感覺好像也沒少寫 code=口=
越來越亂了,好複雜XDD
既然 ModelSchema 的自動轉換相對單純,那為何我還是不建議使用
不過根據文中上面那段話,你最後應該選擇使用了類 pydantic 模式來明示這些 type。
我目前還是很喜歡 FastAPI 和 Ninja(?) 的 pydantic 模式,真的清楚許多!
上面那些隱式規則可能等我以後自己必須用到才去翻文件好了希望一輩子不會用到
這種隱式規則太多的框架大概就是易學難精,簡單使用沒什問題,很輕鬆。
但當你需要應付各種高度客制化的需求,它可能反而會是個障礙,你得花更多時間來熟悉整個框架的用法與讀文件或看 source code才能理解,可能會花更多時間
哈哈哈,你想得太複雜了——也可能是我沒有 get 到你的點。沒關係,我們 high level 地整理一下思路,不必涉及太多細節
Django 專案中,總要定義 ORM 的 Model(相當於 SQLAlchemy 的 Base),也就是我所謂的 Django Model,大概長這樣:
from django.db import models
class Person(models.Model):
# (還有一個 Django 自動加的 id 欄位,不會顯示)
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)
可以看到,CharField 代表字元,相當於 table 欄位的 SQL schema,你能為每一個欄位設定一些參數,這裡只有很簡單的max_length=30
設定,常見的還有null=True
、unique=True
等等
然後,DRF 的 ModelSerializer 會去「閱讀」這些 Django Model 與每一個欄位定義(含參數),自動轉換成序列化器,上面的 Django Model 會被轉換成以下序列化器:
class PersonSerializer(serializers.Serializer):
id = serializers.IntegerField(read_only=True)
first_name = serializers.CharField(max_length=30)
last_name = serializers.CharField(max_length=30)
def create(self, validated_data):
return Person.objects.create(**validated_data)
def update(self, instance, validated_data):
instance.first_name = validated_data.get('first_name', instance.first_name)
instance.last_name = validated_data.get('last_name', instance.last_name)
instance.save()
return instance
是不是幫你做了很多事?XD
但在實際 DRF 專案的程式碼中,你只會看到:
from rest_framework import serializers
from .models import Person
class PersonSerializer(serializers.ModelSerializer):
class Meta:
model = Person
fields = ['id', 'first_name', 'last_name']
是不是隱藏了很多細節?XD
該去DRF文件提 issue 了
我就想說 name 到底是不是指 account_name,還是什麼內部特殊屬性或什參數叫 name (還真不少套件有這樣)
有上面例子這樣看我就懂了
有 from .models import Person
和前面有寫了 Person 這段大概就知道它是另一個檔案寫好的,而這個 Person 裡面內容是對應資料庫的欄位寫的
但在實際 DRF 專案的程式碼中,你只會看到:
不過 Person 也算是你 DRF 專案的一部分吧? 只是是在不同資料夾/檔案下,依然是你自己寫的
因為對照了前一天的文章是有 Post,但沒有 User,沒有寫明完整 import 下有時候不確定哪些是框架內部的,哪些是自己寫的
我改完了!ya~
哈哈哈,文章中舉例時,為了精簡,我很常省略 import 部分,想說反正專案程式碼有,除非是第一次 import,又是重要元件,才會留著
不過 Person 也算是你 DRF 專案的一部分吧? 只是是在不同資料夾/檔案下,依然是你自己寫的
是的,這個例子中,Person 是 Django Model,肯定是自己寫的,只是它被 ModelSerializer 轉成序列化器後,序列化器的欄位參數究竟是長怎樣:
所以,這種隱晦,有時很要命